/**
 * +----------------------------------------------------------------------
 * | 「e家宜业」 —— 助力物业服务升级，用心服务万千业主
 * +----------------------------------------------------------------------
 * | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
 * +----------------------------------------------------------------------
 * | Licensed 未经许可不能去掉「e家宜业」和「卓瓦科技」相关版权
 * +----------------------------------------------------------------------
 * | Author: jixuecong@chowa.cn
 * +----------------------------------------------------------------------
 */

import { format, complementError, asyncMap, warning, deepMerge, convertFieldsError } from './util';
import validators from './validators/index';
import { messages as defaultMessages, newMessages } from './messages';

/**
 *  Encapsulates a validation schema.
 *
 *  @param descriptor An object declaring validation rules
 *  for this schema.
 */
function Validator(descriptor) {
    this.rules = null;
    this._messages = defaultMessages;
    this.define(descriptor);
}

Validator.prototype = {
    messages(messages) {
        if (messages) {
            this._messages = deepMerge(newMessages(), messages);
        }
        return this._messages;
    },
    define(rules) {
        if (!rules) {
            throw new Error('Cannot configure a schema with no rules');
        }
        if (typeof rules !== 'object' || Array.isArray(rules)) {
            throw new Error('Rules must be an object');
        }
        this.rules = {};
        let z;
        let item;
        for (z in rules) {
            if (rules.hasOwnProperty(z)) {
                item = rules[z];
                this.rules[z] = Array.isArray(item) ? item : [item];
            }
        }
    },
    validate(source_, o = {}, oc = () => {}) {
        let source = source_;
        let options = o;
        let callback = oc;
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }
        if (!this.rules || Object.keys(this.rules).length === 0) {
            if (callback) {
                callback();
            }
            return Promise.resolve();
        }

        function complete(results) {
            let i;
            let errors = [];
            let fields = {};

            function add(e) {
                if (Array.isArray(e)) {
                    errors = errors.concat(...e);
                } else {
                    errors.push(e);
                }
            }

            for (i = 0; i < results.length; i++) {
                add(results[i]);
            }
            if (!errors.length) {
                errors = null;
                fields = null;
            } else {
                fields = convertFieldsError(errors);
            }
            callback(errors, fields);
        }

        if (options.messages) {
            let messages = this.messages();
            if (messages === defaultMessages) {
                messages = newMessages();
            }
            deepMerge(messages, options.messages);
            options.messages = messages;
        } else {
            options.messages = this.messages();
        }
        let arr;
        let value;
        const series = {};
        const keys = options.keys || Object.keys(this.rules);
        keys.forEach(z => {
            arr = this.rules[z];
            value = source[z];
            arr.forEach(r => {
                let rule = r;
                if (typeof rule.transform === 'function') {
                    if (source === source_) {
                        source = { ...source };
                    }
                    value = source[z] = rule.transform(value);
                }
                if (typeof rule === 'function') {
                    rule = {
                        validator: rule
                    };
                } else {
                    rule = { ...rule };
                }
                rule.validator = this.getValidationMethod(rule);
                rule.field = z;
                rule.fullField = rule.fullField || z;
                rule.type = this.getType(rule);
                if (!rule.validator) {
                    return;
                }
                series[z] = series[z] || [];
                series[z].push({
                    rule,
                    value,
                    source,
                    field: z
                });
            });
        });
        const errorFields = {};
        return asyncMap(
            series,
            options,
            (data, doIt) => {
                const rule = data.rule;
                let deep =
                    (rule.type === 'object' || rule.type === 'array') &&
                    (typeof rule.fields === 'object' || typeof rule.defaultField === 'object');
                deep = deep && (rule.required || (!rule.required && data.value));
                rule.field = data.field;

                function addFullfield(key, schema) {
                    return {
                        ...schema,
                        fullField: `${rule.fullField}.${key}`
                    };
                }

                function cb(e = []) {
                    let errors = e;
                    if (!Array.isArray(errors)) {
                        errors = [errors];
                    }
                    if (!options.suppressWarning && errors.length) {
                        Validator.warning('async-validator:', errors);
                    }
                    if (errors.length && rule.message !== undefined) {
                        errors = [].concat(rule.message);
                    }

                    errors = errors.map(complementError(rule));

                    if (options.first && errors.length) {
                        errorFields[rule.field] = 1;
                        return doIt(errors);
                    }
                    if (!deep) {
                        doIt(errors);
                    } else {
                        // if rule is required but the target object
                        // does not exist fail at the rule level and don't
                        // go deeper
                        if (rule.required && !data.value) {
                            if (rule.message !== undefined) {
                                errors = [].concat(rule.message).map(complementError(rule));
                            } else if (options.error) {
                                errors = [options.error(rule, format(options.messages.required, rule.field))];
                            }
                            return doIt(errors);
                        }

                        let fieldsValidator = {};
                        if (rule.defaultField) {
                            for (const k in data.value) {
                                if (data.value.hasOwnProperty(k)) {
                                    fieldsValidator[k] = rule.defaultField;
                                }
                            }
                        }
                        fieldsValidator = {
                            ...fieldsValidator,
                            ...data.rule.fields
                        };
                        for (const f in fieldsValidator) {
                            if (fieldsValidator.hasOwnProperty(f)) {
                                const fieldValidator = Array.isArray(fieldsValidator[f])
                                    ? fieldsValidator[f]
                                    : [fieldsValidator[f]];
                                fieldsValidator[f] = fieldValidator.map(addFullfield.bind(null, f));
                            }
                        }
                        const schema = new Validator(fieldsValidator);
                        schema.messages(options.messages);
                        if (data.rule.options) {
                            data.rule.options.messages = options.messages;
                            data.rule.options.error = options.error;
                        }
                        schema.validate(data.value, data.rule.options || options, errs => {
                            const finalErrors = [];
                            if (errors && errors.length) {
                                finalErrors.push(...errors);
                            }
                            if (errs && errs.length) {
                                finalErrors.push(...errs);
                            }
                            doIt(finalErrors.length ? finalErrors : null);
                        });
                    }
                }

                let res;
                if (rule.asyncValidator) {
                    res = rule.asyncValidator(rule, data.value, cb, data.source, options);
                } else if (rule.validator) {
                    res = rule.validator(rule, data.value, cb, data.source, options);
                    if (res === true) {
                        cb();
                    } else if (res === false) {
                        cb(rule.message || `${rule.field} fails`);
                    } else if (res instanceof Array) {
                        cb(res);
                    } else if (res instanceof Error) {
                        cb(res.message);
                    }
                }
                if (res && res.then) {
                    res.then(
                        () => cb(),
                        e => cb(e)
                    );
                }
            },
            results => {
                complete(results);
            }
        );
    },
    getType(rule) {
        if (rule.type === undefined && rule.pattern instanceof RegExp) {
            rule.type = 'pattern';
        }
        if (typeof rule.validator !== 'function' && rule.type && !validators.hasOwnProperty(rule.type)) {
            throw new Error(format('Unknown rule type %s', rule.type));
        }
        return rule.type || 'string';
    },
    getValidationMethod(rule) {
        if (typeof rule.validator === 'function') {
            return rule.validator;
        }
        const keys = Object.keys(rule);
        const messageIndex = keys.indexOf('message');
        if (messageIndex !== -1) {
            keys.splice(messageIndex, 1);
        }
        if (keys.length === 1 && keys[0] === 'required') {
            return validators.required;
        }
        return validators[this.getType(rule)] || false;
    }
};

Validator.register = function register(type, validator) {
    if (typeof validator !== 'function') {
        throw new Error('Cannot register a validator by type, validator is not a function');
    }
    validators[type] = validator;
};

Validator.warning = warning;

Validator.messages = defaultMessages;

Validator.validators = validators;

export default Validator;
